package main

import (
	"Test/xunjian_v3/conf"
	"Test/xunjian_v3/dingtalk"
	"Test/xunjian_v3/tools"
	"encoding/json"
	"fmt"
	"github.com/robfig/cron"
	"log"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

type apiRowData struct {
	Id           string
	Module       string
	Title        string
	Url          string
	Method       string
	Headers      string
	Body         string
	ExpectCode   int
	ExpectData   string
	ExtractData  string
	ErrorCode    string
	ErrorMessage string
}

var (
	wg         sync.WaitGroup
	lock       sync.Mutex
	apiRowsSli []apiRowData
	checkTime  time.Time
)

func checkExcelRow(rowNo int, row []string, token map[string]string, errCount *int, errMsg *string, okCount *int, errRows *[]int, isFinalCheck bool, apiRowsArr []apiRowData) {
	fmt.Printf("checker ==> %v ", rowNo)
	//fmt.Println(row)
	defer wg.Done() //return 时计数器减1

	// 将excel每一列解析出的值赋予各个字段
	id := strings.TrimSpace(row[conf.ColIdNo])
	module := strings.TrimSpace(row[conf.ColModuleNo])
	url := strings.TrimSpace(row[conf.ColUrlNo])
	if conf.Domain != "" && !strings.HasPrefix(url, "http") {
		url = conf.Domain + url
	}
	method := strings.TrimSpace(row[conf.ColMethodNo])
	method = strings.ToUpper(method)
	headers := strings.TrimSpace(row[conf.ColHeadersNo])
	reqBody := strings.TrimSpace(row[conf.ColBodyNo])
	desc := strings.TrimSpace(row[conf.ColDescNo])
	expectCode, _ := strconv.Atoi(row[conf.ColExpectCodeNo])
	expectDataMapStr := strings.TrimSpace(row[conf.ColExpectDataNo])
	extractData := strings.TrimSpace(row[conf.ColExtractNo])

	// 创建一个新的行数据结构，将解析出来的excel数据存入其中
	//var tmpRowData = new(apiRowData)
	apiRowsArr[rowNo].Id = id
	apiRowsArr[rowNo].Module = module
	apiRowsArr[rowNo].Title = desc
	apiRowsArr[rowNo].Url = url
	apiRowsArr[rowNo].Method = method
	apiRowsArr[rowNo].Headers = headers
	apiRowsArr[rowNo].Body = reqBody
	apiRowsArr[rowNo].ExpectCode = expectCode
	apiRowsArr[rowNo].ExpectData = expectDataMapStr
	apiRowsArr[rowNo].ExtractData = extractData
	//apiRowsSli[rowNo].errorCode = 0
	apiRowsArr[rowNo].ErrorMessage = ""

	tmpErrorMessage := ""

	//fmt.Println(method, headers, reqBody, desc, expectCode, expectDataMapStr, extract)

	//替换token
	if token != nil {
		for k, v := range token {
			exp := fmt.Sprintf("\\${%s}", k)
			//fmt.Println(exp, headers)
			r, _ := regexp.Compile(exp)
			//fmt.Println(r.MatchString(headers))
			if r.MatchString(headers) {
				headers = r.ReplaceAllString(headers, v)
			}
			if r.MatchString(url) {
				url = r.ReplaceAllString(url, v)
			}
			if r.MatchString(reqBody) {
				reqBody = r.ReplaceAllString(reqBody, v)
			}
		}
	}

	// 发http起请求
	code, resp, err := tools.RequestUrl(url, method, reqBody, headers)
	//fmt.Println(code, resp)

	if conf.LogMode == "debug" {
		fmt.Printf("API原始返回：%d %+v\n", code, resp)
	}

	// okCount,errCount,errMsg等为多个线程争抢的变量，操作时加锁
	lock.Lock()

	// 错误情况1: 请求失败
	if err != nil {
		fmt.Println(err)
		if !tools.FindIntInSlice(*errRows, rowNo) {
			*errRows = append(*errRows, rowNo)
		}

		if isFinalCheck {
			*errCount++
			if strings.Contains(err.Error(), "imeout") { // Timeout/timeout
				tmpErrorMessage = "请求超时"
			} else if strings.Contains(err.Error(), "no such host") {
				tmpErrorMessage = "域名解析失败"
			} else {
				tmpErrorMessage = "请求访问失败"
			}
			*errMsg += fmt.Sprintf("Desc:%s Message:%s %s\n", desc, tmpErrorMessage, url)
			apiRowsArr[rowNo].ErrorCode = strconv.Itoa(code)
			apiRowsArr[rowNo].ErrorMessage = tmpErrorMessage
		}

		// 争抢的变量操作完毕，解锁
		lock.Unlock()
		return
	}

	//错误情况2: 预期状态码不匹配
	if code != expectCode {
		if !tools.FindIntInSlice(*errRows, rowNo) {
			*errRows = append(*errRows, rowNo)
		}
		fmt.Printf("状态码不匹配，预期：%v 实际：%v，地址：%v\n", expectCode, code, url)
		if isFinalCheck {
			*errCount++
			//*errMsg += fmt.Sprintf("Desc:%s 状态码:%d Message:状态码不符合预期(%d) %s\n", desc, code, expectCode, url)
			tmpErrorMessage = "状态码不符合预期"
			*errMsg += fmt.Sprintf("Desc:%s 状态码:%d %s\n", desc, code, url)
			apiRowsArr[rowNo].ErrorCode = strconv.Itoa(code)
			apiRowsArr[rowNo].ErrorMessage = tmpErrorMessage
		}
		// 争抢的变量操作完毕，解锁
		lock.Unlock()
		return
	}

	// 错误情况3: 状态码匹配，但预期结果不匹配
	expectMatch := true
	//检查返回结果，是否符合excel中预期结果。
	//如果预期不满足，则将expectMatch设置为false，并将返回值的message字段修改为不符合预期的原因
	if expectDataMapStr != "" {
		// 将原始API返回结果解析为json，然后对提取字段进行解析
		response := make(map[string]interface{})
		err = json.Unmarshal([]byte(resp), &response) // 将http请求返回的字符串resp，转化为结构体response
		if err != nil {
			fmt.Println(url, code, "检查预期返回结果，对接口返回body进行json.Unmarshal解析失败")
		}

		// 将excel中的预期结果字段，解析出来
		var expectDataMap map[string]interface{}
		err = json.Unmarshal([]byte(expectDataMapStr), &expectDataMap)
		if err != nil {
			fmt.Println(url, code, "excel中预期结果文本，json.Unmarshal解码失败")
		}
		if response["message"] == nil {
			response["message"] = ""
		}
		// 判断预期结果是否相符
		for k, v := range expectDataMap {
			value, exists := response[k]
			if !exists {
				fmt.Printf("找不到预期的字段，预期：%s\n", k, url)
				expectMatch = false
				tmpErrorMessage = fmt.Sprintf("找不到预期的字段：%s", k)
				response["message"] = fmt.Sprintf("%s %s", response["message"], tmpErrorMessage)
			} else if fmt.Sprintf("%v", value) != fmt.Sprintf("%v", v) {
				fmt.Printf("预期的字段值不匹配，字段：%s，预期：%v，实际：%v，地址：%s\n", k, v, value, url)
				expectMatch = false
				tmpErrorMessage = "预期的字段值不匹配"
				response["message"] = fmt.Sprintf("%s(%s:%v)", response["message"], k, value)
			}
		}

		// 错误情况3: 预期结果不匹配
		if !expectMatch {
			if !tools.FindIntInSlice(*errRows, rowNo) {
				*errRows = append(*errRows, rowNo)
			}
			if isFinalCheck {
				*errCount++
				*errMsg += fmt.Sprintf("Desc:%s  状态码:%d Message:%s %s\n", desc, code, response["message"], url)
				apiRowsArr[rowNo].ErrorCode = strconv.Itoa(code)
				apiRowsArr[rowNo].ErrorMessage = "预期结果不匹配"
			}
			// 争抢的变量操作完毕，解锁
			lock.Unlock()
			return
		}

	}

	// =====================================================================
	// 如果上面错误检查都通过，则为调用成功
	*okCount++
	//调用成功，判断是否需要提取token
	if extractData != "" {
		// 将原始API返回结果解析为json，然后对提取字段进行解析
		response2 := make(map[string]interface{})
		err = json.Unmarshal([]byte(resp), &response2) // 将http请求返回的字符串resp，转化为结构体response
		if err != nil {
			fmt.Println(url, code, "检查预期返回结果，对接口返回body进行json.Unmarshal解析失败")
		}
		extractStr := strings.TrimSpace(row[conf.ColExtractNo])
		if extractStr != "" {
			var extractMap map[string]string
			if err = json.Unmarshal([]byte(extractStr), &extractMap); err != nil {
				fmt.Println(err)
			}

			//fmt.Printf("需要提取数据的API: %s\n", url)
			if conf.LogMode == "debug" {
				fmt.Printf("返回数据: %+v\n", response2)
			}
			fmt.Printf("提取规则: %+v\n", extractMap)

			for k, v := range extractMap {
				respCopy := response2
				str := ""

				//fmt.Println(k, v)
				step := strings.Split(k, ".")
				for i := 1; i < len(step); i++ {
					switch respCopy[step[i]].(type) {
					case map[string]interface{}:
						respCopy = respCopy[step[i]].(map[string]interface{})
					case string:
						str = respCopy[step[i]].(string)
					}
				}
				//将提取的值，写入全局变量
				token[v] = str
				//fmt.Println(str)
				fmt.Printf("提取结果: %+v = %+v\n", v, token[v])
			}
		}
	}
	// 争抢的变量操作完毕，解锁
	lock.Unlock()
	fmt.Println("OK")
	return

}

func checkExcelAll(onlySendError bool) {
	checkTime = time.Now()
	fmt.Println("\n[开启一轮巡检] 开始时间：" + checkTime.Format("2006-01-02 15:04:05"))
	// 从excel中读取所有api数据
	filename := conf.Filename

	rows, err := tools.ReadExcel(filename)
	if err != nil {
		panic(err)
	}

	// 初始化成功计数器，失败计数器，失败返回信息，提取数据
	okCount := 0
	errCount := 0
	errMsg := ""
	token := make(map[string]string)
	errRows := make([]int, 0, 100)
	isFinalCheck := false
	if conf.Retry == 0 {
		isFinalCheck = true
	}
	// 将读取到的rows数据，放入结构体中
	apiRowsSli = make([]apiRowData, len(rows))

	if conf.LogMode == "debug" {
		fmt.Printf("[解析Excel行数据]\n %v\n", rows)
	}

	fmt.Println("\n[step1] 处理需要提取字段的行")

	// step1: 处理需要提取数据的行
	for i, row := range rows {
		if strings.TrimSpace(row[conf.ColExtractNo]) == "" {
			continue
		}

		// 虽然是顺序处理，但每个函数里都要计数器减1，所以这里还是加1
		wg.Add(1)
		fmt.Printf("行号：%d， 用例编号：%v\n", i, row[1])
		checkExcelRow(i, row, token, &errCount, &errMsg, &okCount, &errRows, isFinalCheck, apiRowsSli)
	}
	wg.Wait()
	fmt.Println("[step1] 提取字段结果Map：", token)

	//处理速率限制
	limiter := time.Tick(time.Duration(conf.Checkrate) * time.Millisecond)

	// step2: 处理普通行
	fmt.Println("\n[step2] 处理普通行，对每个API进行巡检")
	for i, row := range rows {
		if strings.TrimSpace(row[conf.ColExtractNo]) != "" {
			continue
		}

		<-limiter // 通过接受阻塞通道，限制了每行处理频率

		// 每一行都丢给一个goroutine执行API巡检工作
		wg.Add(1)
		go checkExcelRow(i, row, token, &errCount, &errMsg, &okCount, &errRows, isFinalCheck, apiRowsSli)
	}
	wg.Wait()

	// step3: 所有接口探测完毕，对出错的接口进行重试
	if conf.Retry > 0 {
		for retry := conf.Retry; retry > 0; retry-- {
			fmt.Printf("对出错的行进行重试：%+v，剩余重试次数：%d\n", errRows, retry)
			for _, errRowNo := range errRows {
				wg.Add(1)
				if retry == 1 {
					isFinalCheck = true
				}
				go checkExcelRow(errRowNo, rows[errRowNo], token, &errCount, &errMsg, &okCount, &errRows, isFinalCheck, apiRowsSli)
			}
			wg.Wait()
		}
	}

	// api接口数据
	if conf.LogMode == "debug" {
		fmt.Println("\n[巡检结果数据]")
		for i, r := range apiRowsSli {
			if r.Id != "" {
				fmt.Printf("%v, %+v\n", i, r)
				//fmt.Printf("%+v\n", r)
			}
		}
	}

	// 计算接口总数、成功率、错误信息
	totalCount := okCount + errCount
	okRate := float32(okCount) / float32(totalCount) * 100
	okRateStr := strconv.Itoa(int(okRate)) + "%"
	if len(errMsg) > 0 {
		errMsg = "\n错误接口信息：\n" + errMsg
	}
	fmt.Printf("\n[总结] sheet: %s total: %d, ok: %d, err: %d, okRate: %s %s\n", conf.Sheet, totalCount, okCount, errCount, okRateStr, errMsg)

	// 根据配置文件判断是否能发送钉钉消息
	//fmt.Println(conf.DingtalkSend)
	if conf.DingtalkSend != "true" {
		fmt.Println("配置文件中定义不需发送钉钉消息")
		return
	}

	// 如果onlySendError，但是调用成功则不发信息
	if onlySendError && (errCount == 0) {
		fmt.Println("全部接口OK，不需发送钉钉")
		return
	}

	// 发送钉钉消息
	dingtalk.Send(totalCount, okCount, errCount, okRateStr, errMsg)

}

//主函数
func main() {
	// 测试模式，执行一轮巡检然后退出
	if conf.Mode == "test" {
		onlySendError := true
		checkExcelAll(onlySendError)
		return
	}
	// 生产模式，循环进行计划任务
	if conf.Mode == "prod" {
		// 利用cron计划任务定时巡检
		cr := cron.New()

		// 加入，每天早上8点25分定时巡检
		err := cr.AddFunc(conf.CronSpecMorning, func() {
			checkExcelAll(false)
		})
		if err != nil {
			fmt.Println(err)
		}

		// 加入，每几分钟巡检，只有报错才发消息
		err = cr.AddFunc(conf.CronSpecInterval, func() {
			checkExcelAll(true)
		})
		if err != nil {
			fmt.Println(err)
		}

		//启动/关闭
		cr.Start()
		defer cr.Stop()

		fmt.Println("server start.")
		http.HandleFunc("/", indexHandler)
		http.HandleFunc("/table", tableHandler)
		http.HandleFunc("/status", apiStatusHandler)
		http.HandleFunc("/time", timeHandler)
		if err := http.ListenAndServe(":1234", nil); err != nil {
			log.Fatal("ListenAndServer:", err)
		}

	}

}
